/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/indent */
/* eslint-disable @typescript-eslint/no-magic-numbers */

// from https://codepen.io/sosuke/pen/Pjoqqp

class Color {
  r!: number
  g!: number
  b!: number

  constructor(r = 255, g = 255, b = 255) {
    this.set(r, g, b)
  }

  toString() {
    return `rgb(${Math.round(this.r)}, ${Math.round(this.g)}, ${Math.round(
      this.b,
    )})`
  }

  set(r: number, g: number, b: number) {
    this.r = this.clamp(r)
    this.g = this.clamp(g)
    this.b = this.clamp(b)
  }

  hueRotate(angle = 0) {
    angle = (angle / 180) * Math.PI
    const sin = Math.sin(angle)
    const cos = Math.cos(angle)

    this.multiply([
      0.213 + cos * 0.787 - sin * 0.213,
      0.715 - cos * 0.715 - sin * 0.715,
      0.072 - cos * 0.072 + sin * 0.928,
      0.213 - cos * 0.213 + sin * 0.143,
      0.715 + cos * 0.285 + sin * 0.14,
      0.072 - cos * 0.072 - sin * 0.283,
      0.213 - cos * 0.213 - sin * 0.787,
      0.715 - cos * 0.715 + sin * 0.715,
      0.072 + cos * 0.928 + sin * 0.072,
    ])
  }

  grayscale(value = 1) {
    this.multiply([
      0.2126 + 0.7874 * (1 - value),
      0.7152 - 0.7152 * (1 - value),
      0.0722 - 0.0722 * (1 - value),
      0.2126 - 0.2126 * (1 - value),
      0.7152 + 0.2848 * (1 - value),
      0.0722 - 0.0722 * (1 - value),
      0.2126 - 0.2126 * (1 - value),
      0.7152 - 0.7152 * (1 - value),
      0.0722 + 0.9278 * (1 - value),
    ])
  }

  sepia(value = 1) {
    this.multiply([
      0.393 + 0.607 * (1 - value),
      0.769 - 0.769 * (1 - value),
      0.189 - 0.189 * (1 - value),
      0.349 - 0.349 * (1 - value),
      0.686 + 0.314 * (1 - value),
      0.168 - 0.168 * (1 - value),
      0.272 - 0.272 * (1 - value),
      0.534 - 0.534 * (1 - value),
      0.131 + 0.869 * (1 - value),
    ])
  }

  saturate(value = 1) {
    this.multiply([
      0.213 + 0.787 * value,
      0.715 - 0.715 * value,
      0.072 - 0.072 * value,
      0.213 - 0.213 * value,
      0.715 + 0.285 * value,
      0.072 - 0.072 * value,
      0.213 - 0.213 * value,
      0.715 - 0.715 * value,
      0.072 + 0.928 * value,
    ])
  }

  multiply(matrix: number[]) {
    const newR = this.clamp(
      this.r * matrix[0] + this.g * matrix[1] + this.b * matrix[2],
    )
    const newG = this.clamp(
      this.r * matrix[3] + this.g * matrix[4] + this.b * matrix[5],
    )
    const newB = this.clamp(
      this.r * matrix[6] + this.g * matrix[7] + this.b * matrix[8],
    )
    this.r = newR
    this.g = newG
    this.b = newB
  }

  brightness(value = 1) {
    this.linear(value)
  }
  contrast(value = 1) {
    this.linear(value, -(0.5 * value) + 0.5)
  }

  linear(slope = 1, intercept = 0) {
    this.r = this.clamp(this.r * slope + intercept * 255)
    this.g = this.clamp(this.g * slope + intercept * 255)
    this.b = this.clamp(this.b * slope + intercept * 255)
  }

  invert(value = 1) {
    this.r = this.clamp((value + (this.r / 255) * (1 - 2 * value)) * 255)
    this.g = this.clamp((value + (this.g / 255) * (1 - 2 * value)) * 255)
    this.b = this.clamp((value + (this.b / 255) * (1 - 2 * value)) * 255)
  }

  hsl() {
    // Code taken from https://stackoverflow.com/a/9493060/2688027, licensed under CC BY-SA.
    const r = this.r / 255
    const g = this.g / 255
    const b = this.b / 255
    const max = Math.max(r, g, b)
    const min = Math.min(r, g, b)
    let h = 0
    let s = 0
    const l = (max + min) / 2

    if (max === min) {
      h = s = 0
    } else {
      const d = max - min
      s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
      switch (max) {
        case r:
          h = (g - b) / d + (g < b ? 6 : 0)
          break

        case g:
          h = (b - r) / d + 2
          break

        case b:
          h = (r - g) / d + 4
          break
      }
      h /= 6
    }

    return {
      h: h * 100,
      s: s * 100,
      l: l * 100,
    }
  }

  clamp(value: number) {
    if (value > 255) {
      value = 255
    } else if (value < 0) {
      value = 0
    }
    return value
  }
}

class Solver {
  target: Color
  targetHSL: ReturnType<Color['hsl']>
  reusedColor: Color

  constructor(target: Color, baseColor?: Color) {
    this.target = target
    this.targetHSL = target.hsl()
    this.reusedColor = baseColor ?? new Color(255, 255, 255)
  }

  solve() {
    const result = this.solveNarrow(this.solveWide())
    return {
      values: result.values,
      loss: result.loss,
      // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
      filter: this.css(result.values),
    }
  }

  solveWide() {
    const A = 5
    const c = 15
    const a = [60, 180, 18000, 600, 1.2, 1.2]

    let best = { loss: Infinity }
    for (let i = 0; best.loss > 25 && i < 3; i++) {
      const initial = [50, 20, 3750, 50, 100, 100]
      const result = this.spsa(A, a, c, initial, 1000)
      if (result.loss < best.loss) {
        best = result
      }
    }
    return { ...best, values: [] }
  }

  solveNarrow(wide: { loss: number; values: number[] }) {
    const A = wide.loss
    const c = 2
    const A1 = A + 1
    const a = [0.25 * A1, 0.25 * A1, A1, 0.25 * A1, 0.2 * A1, 0.2 * A1]
    return this.spsa(A, a, c, wide.values, 500)
  }

  spsa(A: number, a: number[], c: number, values: number[], iters: number) {
    const fix = (value: number, idx: number) => {
      let max = 100
      if (idx === 2 /* saturate */) {
        max = 7500
      } else if (idx === 4 /* brightness */ || idx === 5 /* contrast */) {
        max = 200
      }

      if (idx === 3 /* hue-rotate */) {
        if (value > max) {
          value %= max
        } else if (value < 0) {
          value = max + (value % max)
        }
      } else if (value < 0) {
        value = 0
      } else if (value > max) {
        value = max
      }
      return value
    }
    const alpha = 1
    const gamma = 0.16666666666666666

    let best: number[] = []
    let bestLoss = Infinity
    const deltas: number[] = new Array(6)
    const highArgs: number[] = new Array(6)
    const lowArgs: number[] = new Array(6)

    for (let k = 0; k < iters; k++) {
      const ck = c / Math.pow(k + 1, gamma)
      for (let i = 0; i < 6; i++) {
        deltas[i] = Math.random() > 0.5 ? 1 : -1
        highArgs[i] = values[i] + ck * deltas[i]
        lowArgs[i] = values[i] - ck * deltas[i]
      }

      const lossDiff = this.loss(highArgs) - this.loss(lowArgs)

      for (let i = 0; i < 6; i++) {
        const g = (lossDiff / (2 * ck)) * deltas[i]
        const ak = a[i] / Math.pow(A + k + 1, alpha)
        values[i] = fix(values[i] - ak * g, i)
      }

      const loss = this.loss(values)
      if (loss < bestLoss) {
        best = values.slice(0)
        bestLoss = loss
      }
    }
    return { values: best, loss: bestLoss }
  }

  loss(filters: number[]) {
    // Argument is array of percentages.
    const color = this.reusedColor
    color.set(255, 255, 255)

    color.invert(filters[0] / 100)
    color.sepia(filters[1] / 100)
    color.saturate(filters[2] / 100)
    color.hueRotate(filters[3] * 3.6)
    color.brightness(filters[4] / 100)
    color.contrast(filters[5] / 100)

    const colorHSL = color.hsl()
    return (
      Math.abs(color.r - this.target.r) +
      Math.abs(color.g - this.target.g) +
      Math.abs(color.b - this.target.b) +
      Math.abs(colorHSL.h - this.targetHSL.h) +
      Math.abs(colorHSL.s - this.targetHSL.s) +
      Math.abs(colorHSL.l - this.targetHSL.l)
    )
  }

  css(filters: number[]) {
    const fmt = (idx: number, multiplier = 1) =>
      Math.round(filters[idx] * multiplier)

    return `filter: invert(${fmt(0)}%) sepia(${fmt(1)}%) saturate(${fmt(
      2,
    )}%) hue-rotate(${fmt(3, 3.6)}deg) brightness(${fmt(4)}%) contrast(${fmt(
      5,
    )}%);`
  }
}

function hexToRgb(hex: string): number[] {
  // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
  const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i
  hex = hex.replace(
    shorthandRegex,
    (m, r: string, g: string, b: string) => `${r}${r}${g}${g}${b}${b}`,
  )

  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
  return result
    ? [
        parseInt(result[1], 16),

        parseInt(result[2], 16),
        parseInt(result[3], 16),
      ]
    : [0, 0, 0]
}

export const hexToFilter = (
  input: string,
  base?: string,
): {
  lossMsg: string
  result: { values: number[]; loss: number; filter: string }
} => {
  const rgb = hexToRgb(input)
  if (rgb?.length !== 3) {
    throw new Error('Invalid format!')
  }

  const color = new Color(rgb[0], rgb[1], rgb[2])
  let solver = new Solver(color)
  if (base) {
    const baseHex = hexToRgb(base)
    const baseColor = new Color(baseHex[0], baseHex[1], baseHex[2])
    solver = new Solver(baseColor)
  }

  const result = solver.solve()

  let lossMsg
  if (result.loss < 1) {
    lossMsg = 'This is a perfect result.'
  } else if (result.loss < 5) {
    lossMsg = 'The is close enough.'
  } else if (result.loss < 15) {
    lossMsg = 'The color is somewhat off. Consider running it again.'
  } else {
    lossMsg = 'The color is extremely off. Run it again!'
  }

  return { lossMsg, result }
}
